1

注:原文发表于 2015年 12月20日,彼时一些相关的技术方案还处于公测或者论证阶段。

Rich Harris’ module bundler Rollup popularized an important feature in the JavaScript world: tree-shaking, excluding unused exports from bundles. Rollup depends on the static structure of ES6 modules (imports and exports can’t be changed at runtime) to detect which exports are unused.

Rich Harris 的模块打包器 Rollup 在 javascript 的圈子里引爆了一个新的概念 : tree-shaking。 tree shaking 在模块打包时排除没有使用到的模块,从而减小代码体积,提高加载和执行效率。 Rollup 依赖 ES6 的静态模块结构(不像ES6中可以在runtime中动态的决定导入导出的内容) 分析辨别无用的代码。

Tree-shaking for webpack is currently in beta. This blog post explains how it works. The project we are going to examine is on GitHub: tree-shaking-demo

webpack 的 tree-shaking 方案目前处于公测阶段。本篇博文解释它是如何工作的。文中使用到的代码来自于 tree-shaking-demo

webpack2 如何消除无用源代码

webpack 2, a new version that is in beta, eliminates unused exports in two steps:

webpack2 通过下面的两个步骤来消除无用的代码。

First, all ES6 module files are combined into a single bundle file. In that file, exports that were not imported anywhere are not exported, anymore.

首先,所有的 ES6 文件整合到一个 bundle 中,在这个文件里,没有被导入过的模块将不会再被导出。

我的理解是:

m1.js

export const foo = () => {}
export const bar = () => {}

m2.js

import { foo } from './m1'

foo()

整合过程中,发现 bar 没有被其他模块导入过,所以最终结果是

bundle.js

export const foo = () => {}
const bar = () => {} // bar 还在,只是没有被 export

foo()

Second, the bundle is minified, while eliminating dead code. Therefore, entities that are neither exported nor used inside their modules do not appear in the minified bundle. Without the first step, dead code elimination would never remove exports (registering an export keeps it alive).

移除既不导出也不使用的模块。如果没有第一步的支持,我们无法辨别从未被导入过的模块。

Unused exports can only be reliably detected at build time if the module system has a static structure. Therefore, webpack 2 can parse and understand all of ES6 and only tree-shakes if it detects an ES6 module. However, only imports and exports are transpiled to ES5. If you want all of the bundle to be in ES5, you need a transpiler for the remaining parts of ES6. In this blog post, we’ll use Babel 6.

只有在静态的模块结构下,才能够准确的移除无用的模块。 webpack 2 仅仅能够理解和分析 ES6 的模块并且执行 tree-shaking,并不能帮你将代码编译成 ES6,如果你需要那么做,可以使用 Babel 或者其他的编译工具。

输入:ES6 代码

示例包含两个 ES6 代码文件。

helpers.js

// helpers.js
export function foo() {
    return 'foo';
}
export function bar() {
    return 'bar';
}

main.js, 入口文件

// main.js
import {foo} from './helpers';

let elem = document.getElementById('output');
elem.innerHTML = `Output: ${foo()}`;

注意 bar 模块没有被任何其他模块使用。

输出:没有 tree-shaking

Babel6 编译 ES6 代码最常见办法的是使用一个预设的 preset babel-preset-es2015 :

{
    presets: ['es2015'],
}

However, that preset includes the plugin transform-es2015-modules-commonjs, which means that Babel will output CommonJS modules and webpack won’t be able to tree-shake:

然而,这个 preset 包含 transform-es2015-modules-commonjs,它会使得 Babel 输出 commonjs 风格的模块,这导致 webpack 无法执行 tree-shake。

function(module, exports) {

    'use strict';

    Object.defineProperty(exports, "__esModule", {
        value: true
    });
    exports.foo = foo;
    exports.bar = bar;
    function foo() {
        return 'foo';
    }
    function bar() {
        return 'bar';
    }

}

You can see that bar is part of the exports, which prevents it being recognized as dead code by minification.

你可以看到 bar 现在是 exports 的一部分了, webpack 无法辨别 bar 是否是 dead code - 从来没有被使用的代码。

输出: tree-shaking

What we want is Babel’s es2015, but without the plugin transform-es2015-modules-commonjs. At the moment, the only way to get that is by mentioning all of the preset’s plugins in our configuration data, except for the one we want to exclude. The preset’s source is on GitHub, so it’s basically a case of copying and pasting:

我们想要不包含 transform-es2015-modules-commonjs 的 babel-preset-es2015 ,目前来说只能显示的声明一堆 plugin 来代替 preset。如下。

{
    plugins: [
        'transform-es2015-template-literals',
        'transform-es2015-literals',
        'transform-es2015-function-name',
        'transform-es2015-arrow-functions',
        'transform-es2015-block-scoped-functions',
        'transform-es2015-classes',
        'transform-es2015-object-super',
        'transform-es2015-shorthand-properties',
        'transform-es2015-computed-properties',
        'transform-es2015-for-of',
        'transform-es2015-sticky-regex',
        'transform-es2015-unicode-regex',
        'check-es2015-constants',
        'transform-es2015-spread',
        'transform-es2015-parameters',
        'transform-es2015-destructuring',
        'transform-es2015-block-scoping',
        'transform-es2015-typeof-symbol',
        ['transform-regenerator', { async: false, asyncGenerators: false }],
    ],
}

If we build the project now, module helpers looks like this inside the bundle:

如果我们再去编译这个项目, 会发现一些变化。

function(module, exports, __webpack_require__) {

    /* harmony export */ exports["foo"] = foo;
    /* unused harmony export bar */;

    function foo() {
        return 'foo';
    }
    function bar() {
        return 'bar';
    }
}

Only foo is an export now, but bar is still there. After minification, helpers looks like this (I’ve added line breaks and whitespace to make the code easier to read):

简单来说, 只有 foo 被导出。接下来做移除的时候,打包工具发现 bar 可以移除,从而得到下面的结果。

function (t, n, r) {
    function e() {
        return "foo"
    }

    n.foo = e
}

Et voilà – no more function bar!

再也没有 bar 了。

拓展阅读


驽马
1.3k 声望26 粉丝

前端/android/入门级设计